package com.imooc.coupon.service.impl;

import com.alibaba.fastjson.JSON;
import com.imooc.coupon.constant.CouponChannel;
import com.imooc.coupon.constant.CouponStatus;
import com.imooc.coupon.dao.CouponDao;
import com.imooc.coupon.entity.Coupon;
import com.imooc.coupon.exception.CouponException;
import com.imooc.coupon.feign.SettlementClient;
import com.imooc.coupon.feign.TemplateClient;
import com.imooc.coupon.service.IRedisService;
import com.imooc.coupon.service.IStreamProducer;
import com.imooc.coupon.service.IUserService;
import com.imooc.coupon.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageChannel;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * <h1>用户服务相关的接口实现</h1>
 * 所有的操作过程, 状态都保存在 Redis 中, 并通过 MQ 把消息传递到 MySQL 中
 */
@Slf4j
@Service
public class UserServiceImpl implements IUserService {

    /** Coupon Dao */
    @Autowired
    private CouponDao couponDao;

    /** Redis 服务 */
    @Autowired
    private IRedisService redisService;

    /** 模板微服务客户端 */
    @Autowired
    private TemplateClient templateClient;

    /** 结算微服务客户端 */
    @Autowired
    private SettlementClient settlementClient;

    @Autowired
    IStreamProducer streamProducer;

    /**
     * <h2>根据用户 id 和优惠券状态查询优惠券记录</h2>
     * 注意点： 1、隐含可使用优惠券的过期延迟处理，如判断为已过期，则要更新缓存及数据库状态。
     *         2、缓存中有个假优惠券，目的是为了防止缓存穿透，返回时需要剔除
     *  流程：
     *  1、查询缓存，如果缓存中没有，则从数据库中查询，并将结果放入缓存。
     *  2、返回值中需要剔除用于防止缓存穿透的假优惠券
     *  3、查询的是可用优惠券时，需要刷选出过期券，将过期卷更新到缓存及数据库
     *
     * @param userId 用户 id
     * @param status 优惠券状态
     * @return {@link Coupon}s
     */
    @Override
    public List<Coupon> findCouponsByStatus(Long userId, Integer status) throws CouponException {

        // 从缓存获取
        List<Coupon> curCached = redisService.getCachedCoupons(userId, status);
        List<Coupon> preTarget;

        if(CollectionUtils.isNotEmpty(curCached)){
            log.debug("coupon cache is not empty: {}, {}", userId, status);
            preTarget = curCached;
        }else{
            // 从数据库获取
            log.debug("coupon cache is empty, get coupon from db: {}, {}",
                    userId, status);
            List<Coupon> dbCoupons = couponDao.findAllByUserIdAndStatus(userId, CouponStatus.of(status));

            // 如果数据库中没有记录, 直接返回就可以, Cache 中已经加入了一张无效的优惠券
            if (CollectionUtils.isEmpty(dbCoupons)) {
                log.debug("current user do not have coupon: {}, {}", userId, status);
                return dbCoupons;
            }
            // 放入缓存
            // 填充 dbCoupons的 templateSDK 字段
            Map<Integer, CouponTemplateSDK> id2TemplateSDK = templateClient.findIds2TemplateSDK(dbCoupons.stream()
                    .map(Coupon::getTemplateId)
                    .collect(Collectors.toList())
            ).getData();
            dbCoupons.stream().forEach(c->c.setTemplateSDK(
                    id2TemplateSDK.get(c.getTemplateId()))
            );

            preTarget = dbCoupons;
            redisService.addCouponToCache(userId,preTarget,status);

        }
        //剔除用于防止缓存穿透的假优惠券
        preTarget = preTarget.stream()
                .filter(c -> c.getId()!=-1)
                .collect(Collectors.toList());

        // 如果当前获取的是可用优惠券, 还需要做对已过期优惠券延迟处理
        if(CouponStatus.of(status) == CouponStatus.USABLE){
            CouponClassify classify = CouponClassify.classify(preTarget);
            // 如果已过期状态不为空, 需要做延迟处理
            if(CollectionUtils.isNotEmpty(classify.getExpired())){
                log.info("Add Expired Coupons To Cache From FindCouponsByStatus: " +
                        "{}, {}", userId, status);
                redisService.addCouponToCache(userId,classify.getExpired(),CouponStatus.EXPIRED.getCode());
                // 发送消息做异步更新DB
                CouponMessage couponMessage = new CouponMessage();
                couponMessage.setStatus(CouponStatus.EXPIRED.getCode());
                couponMessage.setIds(classify.getExpired()
                        .stream()
                        .map(Coupon::getId)
                        .collect(Collectors.toList())
                );
                streamProducer.sendMessage(CouponChannel.COUPON_MESSAGE_OUTPUT,couponMessage);
                return classify.getUsable();
            }
        }
        return preTarget;
    }

    /**
     * <h2>根据用户 id 查找当前可以领取的优惠券模板</h2>
     * 注意点：
     * 1、模板的状态是由定时任务更新的，存在延迟，所以获取模板后需要根据有效期判断是否真实可用。
     * 2、对于未过期的优惠券模板，需要根据领取数量限制，进一步判断用户是否可用该模板。
     *
     * @param userId 用户 id
     * @return {@link CouponTemplateSDK}s
     */
    @Override
    public List<CouponTemplateSDK> findAvailableTemplate(Long userId) throws CouponException {
        // 获取优惠券模板
        List<CouponTemplateSDK> templateSDKs = templateClient.findAllUsableTemplate().getData();
        log.debug("Find All Template(From TemplateClient) Count: {}",
                templateSDKs.size());
        // 过滤过期的优惠券模板
        long curTime = new Date().getTime();
        templateSDKs = templateSDKs.stream().filter(t -> t.getRule().getExpiration().getDeadline() > curTime)
                .collect(Collectors.toList());
        log.info("Find Unexpired Template Count: {}", templateSDKs.size());

        // 过滤已达领取数量上限优惠券模板
        // 拿到可用优惠券，这里只拿可用优惠券进行数量判断感觉有点问题，难道不用考虑已使用的优惠券吗？
        List<Coupon> userUsableCoupons = this.findCouponsByStatus(userId,CouponStatus.USABLE.getCode());
        // 按templateId分组
        Map<Integer, List<Coupon>> templateId2Coupons = userUsableCoupons
                                                .stream()
                                                .collect(Collectors.groupingBy(Coupon::getTemplateId));
        List<CouponTemplateSDK> result = new ArrayList<>(templateSDKs.size());
        templateSDKs.stream().forEach(t -> {
            Integer templateId = t.getId();
            Integer limitation = t.getRule().getLimitation();
            if(templateId2Coupons.get(templateId)==null || templateId2Coupons.get(templateId).size()<limitation){
                // 没有领过，或领取的数量还未到限制
                result.add(t);
            }
        });

        return result;
    }

    /**
     * <h2>用户领取优惠券</h2>
     * 1. 从 TemplateClient 拿到对应的优惠券模板, 并检查是否过期
     * 2. 根据 limitation 判断用户是否可以领取
     * 3. save to db
     * 4. 填充 CouponTemplateSDK
     * 5. save to cache
     * @param request {@link AcquireTemplateRequest}
     * @return {@link Coupon}
     */
    @Override
    public Coupon acquireTemplate(AcquireTemplateRequest request) throws CouponException {
        Long userId = request.getUserId();
        CouponTemplateSDK templateSDK = request.getTemplateSDK();
        Integer templateId = request.getTemplateSDK().getId();
        //1、 领取优惠券前的校验工作

        // 拿到对应的优惠券模板
        Map<Integer, CouponTemplateSDK> id2TemplateSDK = templateClient.findIds2TemplateSDK(Collections.singletonList(templateId)).getData();

        if (id2TemplateSDK.size() <= 0) {
            log.error("Can Not Acquire Template From TemplateClient: {}",
                    request.getTemplateSDK().getId());
            throw new CouponException("Can Not Acquire Template From TemplateClient");
        }

        // 判断是否过期
        Long deadline = id2TemplateSDK.get(templateId).getRule().getExpiration().getDeadline();
        long curTime = (new Date()).getTime();
        if(deadline<curTime){
            log.error("Template is expired {}",
                    request.getTemplateSDK().getId());
            throw new CouponException("Template is expired");
        }

        // 用户是否可以领取这张优惠券
        List<Coupon> userUsableCoupons = findCouponsByStatus(
                request.getUserId(), CouponStatus.USABLE.getCode()
        );
        Integer limitation = templateSDK.getRule().getLimitation();
        List<Coupon> takedCoupons = userUsableCoupons.stream().filter(c -> c.getTemplateId() == templateId).collect(Collectors.toList()); // 用户在该模板已领取的优惠券
        if(CollectionUtils.isNotEmpty(takedCoupons) && takedCoupons.size()>=limitation){
            // 已达到领取上限
            log.error("Exceed Template Assign Limitation: {}",
                    request.getTemplateSDK().getId());
            throw new CouponException("Exceed Template Assign Limitation");
        }

        //2、 实际领取优惠券的代码

        // 尝试去获取优惠券码
        String couponCode = redisService.tryToAcquireCouponCodeFromCache(templateId);
        if (StringUtils.isEmpty(couponCode)) {
            log.error("Can Not Acquire Coupon Code: {}",
                    request.getTemplateSDK().getId());
            throw new CouponException("Can Not Acquire Coupon Code");
        }

        // save to db
        Coupon newCoupon = new Coupon(templateId,userId,couponCode,CouponStatus.USABLE);
        newCoupon = couponDao.save(newCoupon);

        // 填充 Coupon 对象的 CouponTemplateSDK, 一定要在放入缓存之前去填充
        newCoupon.setTemplateSDK(request.getTemplateSDK());

        // 放入缓存中
        redisService.addCouponToCache(userId, Collections.singletonList(newCoupon),CouponStatus.USABLE.getCode());

        return newCoupon;
    }

    /**
     * <h2>结算(核销)优惠券</h2>
     * 这里需要注意, 规则相关处理需要由 Settlement 微服务去做, 当前系统仅仅做校验及优惠券业务处理
     *
     * @param info {@link SettlementInfo}  Cost结算金额为0
     * @return {@link SettlementInfo} Cost结算金额为商品总额经过优惠券计算过的最终金额值。
     */
    @Override
    public SettlementInfo settlement(SettlementInfo info) throws CouponException {
        // 当没有传递优惠券时, 直接返回商品总价
        List<SettlementInfo.CouponAndTemplateInfo> couponAndTemplateInfos = info.getCouponAndTemplateInfos();
        if(CollectionUtils.isEmpty(couponAndTemplateInfos)){
            log.info("Empty Coupons For Settle.");
            double goodsSum = 0.0;
            for(GoodsInfo gi : info.getGoodsInfos()){
                goodsSum += gi.getPrice()*gi.getCount();
            }
            // 没有优惠券也就不存在优惠券的核销, SettlementInfo 其他的字段不需要修改
            info.setCost(retain2Decimals(goodsSum));
            return info;
        }

        // 校验传递的优惠券是否是用户自己的
        List<Coupon> coupons = findCouponsByStatus(info.getUserId(), CouponStatus.USABLE.getCode());
        List<Integer> couponIds = coupons.stream().map(Coupon::getId).collect(Collectors.toList());
        List<Integer> couponIdsToUse = couponAndTemplateInfos.stream().map(ct -> ct.getId()).collect(Collectors.toList());
        // 判断腰使用的优惠券是否包含在用户领取的优惠券中
        if(CollectionUtils.isEmpty(couponIds) || !CollectionUtils.isSubCollection(couponIdsToUse,couponIds)){
            log.info("{}", couponIds);
            log.info("{}", couponIdsToUse);
            log.error("User Coupon Has Some Problem, It Is Not SubCollection" +
                    "Of Coupons!");
            throw new CouponException("User Coupon Has Some Problem, " +
                    "It Is Not SubCollection Of Coupons!");
        }

        // 结算优惠券
        Map<Integer, Coupon> id2Coupon = coupons.stream()
                                .collect(Collectors.toMap(
                                        Coupon::getId, Function.identity())
                                );
        List<Coupon> settleCoupons = new ArrayList<>(coupons.size());
        couponAndTemplateInfos.forEach(ci -> settleCoupons.add(id2Coupon.get(ci.getId())));
        // 通过结算服务获取结算信息
        SettlementInfo processedInfo = settlementClient.computeRule(info).getData();
        // 结果SettlementInfo中的CouponAndTemplateInfos是计算时实际使用的优惠券
        if (CollectionUtils.isNotEmpty(processedInfo.getCouponAndTemplateInfos())
         && processedInfo.getCost()!=-1.0){ // 这里可以多加个判断属性processedInfo.getEmploy()，来决定是核销还是结算。结算就不必更新数据库
            log.info("Settle User Coupon: {}, {}", info.getUserId(),
                    JSON.toJSONString(settleCoupons));
            // 更新缓存
            redisService.addCouponToCache(info.getUserId(),settleCoupons,CouponStatus.USED.getCode());
            // 更新 db
            CouponMessage couponMessage = new CouponMessage();
            couponMessage.setStatus(CouponStatus.USED.getCode());
            couponMessage.setIds(settleCoupons.stream().map(Coupon::getId).collect(Collectors.toList()));
            streamProducer.sendMessage(CouponChannel.COUPON_MESSAGE_OUTPUT,couponMessage);
        }

        return processedInfo;
    }

    /**
     * <h2>保留两位小数</h2>
     * */
    private Double retain2Decimals(double value) {
        // BigDecimal.ROUND_HALF_UP 代表四舍五入
        return new BigDecimal(value)
                .setScale(2,BigDecimal.ROUND_HALF_UP)
                .doubleValue();
    }


}












